//! Minimal test runner utilities for the `nrc test` CLI.

use crate::ast::Declaration;
use crate::error::{CompilerError, Result};
use crate::parser;
use crate::{Compiler, CompilerConfig, OptLevel};
use glob::Pattern;
use std::fs;
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use std::process::Command;
use tempfile::TempDir;
use walkdir::WalkDir;

/// Summary information returned after executing tests.
pub struct TestSummary {
    /// Total number of executed tests.
    pub total: usize,
    /// Number of successful tests.
    pub passed: usize,
    /// Number of failed tests.
    pub failed: usize,
}

/// Run tests from either a file or a directory recursively.
pub fn run_path_tests(path: &Path, filter: Option<&str>) -> Result<TestSummary> {
    let compiled_filter = match filter {
        Some(raw) => Some(TestFilter::new(raw)?),
        None => None,
    };

    if path.is_dir() {
        run_directory_tests(path, compiled_filter.as_ref())
    } else {
        run_file_tests(path, compiled_filter.as_ref())
    }
}

/// Run all tests defined inside a single `_test.nr` file.
fn run_file_tests(path: &Path, filter: Option<&TestFilter>) -> Result<TestSummary> {
    let abs_path = to_absolute_path(path)?;
    let source = fs::read_to_string(&abs_path)
        .map_err(|_| CompilerError::file_not_found(abs_path.to_string_lossy().to_string()))?;

    let mut tests = discover_tests(&source, &abs_path).map_err(|e| {
        CompilerError::internal(&format!(
            "Failed to discover tests in {}: {}",
            abs_path.display(),
            e
        ))
    })?;
    if tests.is_empty() {
        println!(
            "No tests found in {}",
            abs_path.file_name().unwrap_or_default().to_string_lossy()
        );
        return Ok(TestSummary {
            total: 0,
            passed: 0,
            failed: 0,
        });
    }

    if let Some(filter) = filter {
        tests.retain(|name| filter.matches(name));
        if tests.is_empty() {
            println!(
                "No tests matched filter '{}' in {}",
                filter.display_pattern(),
                display_name(&abs_path)
            );
            return Ok(TestSummary {
                total: 0,
                passed: 0,
                failed: 0,
            });
        }
    }

    println!(
        "Running {} test(s) from {}",
        tests.len(),
        abs_path.display()
    );

    let mut passed = 0usize;
    let mut failed = 0usize;
    let mut failed_tests = Vec::new();
    let mut error_tests = Vec::new();
    for test_name in tests {
        print!("test {}::{} ... ", display_name(&abs_path), test_name);
        io::stdout().flush().ok();

        match compile_and_run_test(&source, &abs_path, &test_name) {
            Ok(true) => {
                println!("ok");
                passed += 1;
            }
            Ok(false) => {
                println!("FAILED");
                failed += 1;
                failed_tests.push((display_name(&abs_path), test_name.clone()));
            }
            Err(e) => {
                println!("ERROR");
                let error_msg = format!("{}", e);
                error_tests.push((display_name(&abs_path), test_name.clone(), error_msg));
            }
        }
    }

    println!(
        "\nSummary: {} passed; {} failed; {} total",
        passed,
        failed + error_tests.len(),
        passed + failed + error_tests.len()
    );

    if !failed_tests.is_empty() {
        println!("\nFailed tests:");
        for (file_name, test_name) in &failed_tests {
            println!("  - {}::{}", file_name, test_name);
        }
    }

    if !error_tests.is_empty() {
        println!("\nError tests (compilation/runtime errors):");
        for (file_name, test_name, error_msg) in &error_tests {
            println!("  - {}::{}", file_name, test_name);
            println!("    Error: {}", error_msg);
        }
        // Don't return error here - let the summary be printed
        // The error will be handled in main.rs if needed
    }

    Ok(TestSummary {
        total: passed + failed + error_tests.len(),
        passed,
        failed: failed + error_tests.len(),
    })
}

fn run_directory_tests(dir: &Path, filter: Option<&TestFilter>) -> Result<TestSummary> {
    let mut aggregate = TestSummary {
        total: 0,
        passed: 0,
        failed: 0,
    };
    let mut found = false;

    for entry in WalkDir::new(dir).into_iter().filter_map(|e| e.ok()) {
        let path = entry.path();
        if path.is_file() && is_test_file(path) {
            found = true;
            match run_file_tests(path, filter) {
                Ok(summary) => {
                    aggregate.total += summary.total;
                    aggregate.passed += summary.passed;
                    aggregate.failed += summary.failed;
                }
                Err(e) => {
                    eprintln!("Error processing {}: {}", path.display(), e);
                    return Err(CompilerError::internal(&format!(
                        "Failed to run tests in {}: {}",
                        path.display(),
                        e
                    )));
                }
            }
        }
    }

    if !found {
        println!("No *_test.nr files found under {}", dir.to_string_lossy());
    } else {
        println!(
            "\nOverall: {} passed; {} failed; {} total\n",
            aggregate.passed, aggregate.failed, aggregate.total
        );
    }

    Ok(aggregate)
}

enum TestFilter {
    Substring(String),
    Glob { original: String, pattern: Pattern },
}

impl TestFilter {
    fn new(raw: &str) -> Result<Self> {
        if contains_glob_char(raw) {
            let pattern = Pattern::new(raw).map_err(|e| {
                CompilerError::internal(&format!("Invalid filter pattern '{}': {}", raw, e))
            })?;
            Ok(TestFilter::Glob {
                original: raw.to_string(),
                pattern,
            })
        } else {
            Ok(TestFilter::Substring(raw.to_string()))
        }
    }

    fn matches(&self, name: &str) -> bool {
        match self {
            TestFilter::Substring(substr) => name.contains(substr),
            TestFilter::Glob { pattern, .. } => pattern.matches(name),
        }
    }

    fn display_pattern(&self) -> &str {
        match self {
            TestFilter::Substring(substr) => substr,
            TestFilter::Glob { original, .. } => original,
        }
    }
}

fn contains_glob_char(raw: &str) -> bool {
    raw.chars().any(|c| matches!(c, '*' | '?' | '[' | ']'))
}

fn to_absolute_path(path: &Path) -> Result<PathBuf> {
    if path.is_absolute() {
        Ok(path.to_path_buf())
    } else {
        Ok(std::env::current_dir()
            .unwrap_or_else(|_| PathBuf::from("."))
            .join(path))
    }
}

fn discover_tests(source: &str, file_path: &Path) -> Result<Vec<String>> {
    let mut parser = parser::Parser::new(
        source.to_string(),
        Some(file_path.to_string_lossy().to_string()),
    );
    let program = parser.parse_program()?;

    let mut tests = Vec::new();
    let mut has_main = false;
    for decl in program.declarations {
        if let Declaration::Function(func) = decl {
            if func.name == "main" {
                has_main = true;
                continue;
            }
            if is_test_function(&func) {
                tests.push(func.name.clone());
            }
        }
    }

    if has_main {
        println!(
            "Skipping {} (contains top-level 'main'; remove it to use `nrc test`)",
            display_name(file_path)
        );
        return Ok(Vec::new());
    }

    Ok(tests)
}

fn is_test_function(func: &crate::ast::FunctionDecl) -> bool {
    func.name.starts_with("test") && func.parameters.is_empty() && func.return_type.is_none()
}

fn compile_and_run_test(source: &str, file_path: &Path, test_name: &str) -> Result<bool> {
    let runner_source = generate_runner_source(source, test_name);

    let module_name = file_path
        .file_stem()
        .and_then(|s| s.to_str())
        .unwrap_or("main");

    let temp_dir = TempDir::new().map_err(|e| CompilerError::internal(&e.to_string()))?;
    let output_name = format!("{}_{}", module_name, test_name);
    let mut config = CompilerConfig::default();
    config.output_dir = temp_dir.path().to_string_lossy().to_string();
    config.output_name = output_name.clone();
    config.target_arch = std::env::consts::ARCH.to_string();
    config.target_os = host_target_os();
    config.opt_level = OptLevel::Basic;
    config.debug_info = false;
    config.static_link = true;

    let compiler = Compiler::new(config);
    compiler
        .compile_source(&runner_source, Some(module_name), Some(file_path))
        .map_err(|e| {
            CompilerError::internal(&format!(
                "Failed to compile test {}::{}: {}",
                display_name(file_path),
                test_name,
                e
            ))
        })?;

    let executable = temp_dir.path().join(output_name);
    let status = Command::new(&executable).status().map_err(|e| {
        CompilerError::internal(&format!(
            "Failed to execute test binary {}: {}",
            executable.display(),
            e
        ))
    })?;

    Ok(status.success())
}

fn generate_runner_source(original: &str, test_name: &str) -> String {
    let mut result = String::with_capacity(original.len() + 256);
    result.push_str(original);
    if !original.ends_with('\n') {
        result.push('\n');
    }
    result.push_str("\nfunc main() int {\n");
    result.push_str("    ");
    result.push_str(test_name);
    result.push_str("();\n");
    result.push_str("    return 0\n");
    result.push_str("}\n");
    result
}

fn display_name(path: &Path) -> String {
    path.file_name()
        .and_then(|s| s.to_str())
        .unwrap_or_default()
        .to_string()
}

fn host_target_os() -> String {
    match std::env::consts::OS {
        "macos" => "darwin".to_string(),
        other => other.to_string(),
    }
}

fn is_test_file(path: &Path) -> bool {
    if !path
        .extension()
        .and_then(|ext| ext.to_str())
        .map(|ext| ext.eq_ignore_ascii_case("nr"))
        .unwrap_or(false)
    {
        return false;
    }
    path.file_name()
        .and_then(|name| name.to_str())
        .map(|name| name.ends_with("_test.nr"))
        .unwrap_or(false)
}
